package com.xhj.mybatis.builder.xml;

import com.xhj.mybatis.builder.BaseBuilder;
import com.xhj.mybatis.builder.MapperBuilderAssistant;
import com.xhj.mybatis.executor.keygen.Jdbc3KeyGenerator;
import com.xhj.mybatis.executor.keygen.KeyGenerator;
import com.xhj.mybatis.executor.keygen.NoKeyGenerator;
import com.xhj.mybatis.executor.keygen.SelectKeyGenerator;
import com.xhj.mybatis.mapping.MappedStatement;
import com.xhj.mybatis.mapping.SqlCommandType;
import com.xhj.mybatis.mapping.SqlSource;
import com.xhj.mybatis.scripting.LanguageDriver;
import com.xhj.mybatis.session.Configuration;
import org.dom4j.Element;

import java.util.List;
import java.util.Locale;

/**
 * XMLStatementBuilder
 *
 * @author XJks
 * @description 基于XML的MappedStatement构建器
 */
public class XMLStatementBuilder extends BaseBuilder {

    /**
     * Mapper 构建器助手
     */
    private final MapperBuilderAssistant builderAssistant;

    /**
     * XxxMapper.xml 文件中的 select|insert|update|delete 节点
     */
    private final Element context;

    /**
     * 所需的数据库标识符
     */
    private final String requiredDatabaseId;

    /**
     * 构造方法1：不指定 databaseId，默认为 null
     *
     * @param configuration    MyBatis 配置对象
     * @param builderAssistant Mapper 构建器助手
     * @param context          XML 节点上下文
     */
    public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, Element context) {
        // 调用重载构造方法，传递 null 作为 databaseId
        this(configuration, builderAssistant, context, null);
    }

    /**
     * 构造方法2：可以指定 databaseId
     *
     * @param configuration    MyBatis 配置对象
     * @param builderAssistant Mapper 构建器助手
     * @param context          XML 节点上下文
     * @param databaseId       所需的数据库标识符
     */
    public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, Element context, String databaseId) {
        // 调用父类 BaseBuilder 的构造方法，初始化 configuration
        super(configuration);
        // 保存 MapperBuilderAssistant 实例，用于辅助构建 Mapper 相关对象
        this.builderAssistant = builderAssistant;
        // 保存当前 XML 节点的上下文信息
        this.context = context;
        // 保存需要匹配的 databaseId（可以为 null，表示不限定数据库）
        this.requiredDatabaseId = databaseId;
    }

    /**
     * 解析 <select> <insert> <update> <delete> 语句节点
     */
    public void parseStatementNode() {
        // 获取 id 属性
        String id = context.attributeValue("id");
        // 获取 databaseId 属性
        String databaseId = context.attributeValue("databaseId");
        // 判断当前 databaseId 是否匹配，不匹配则直接返回，不解析该节点
        if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
            return;
        }
        // 获取 SQL 语句的类型（SELECT、INSERT、UPDATE、DELETE）
        String nodeName = context.getName();
        // 将节点名称转为 SqlCommandType 枚举（如 SELECT、INSERT、UPDATE、DELETE）
        SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
        // 判断是否为 select 语句
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
        // 获取 flushCache 属性，select 默认为 false，其他为 true
        Boolean flushCacheTemp = getBooleanAttributeOfDom4jElement(context, "flushCache");
        boolean flushCache = flushCacheTemp != null ? flushCacheTemp : !isSelect;
        // 获取 parameterType 属性的值，并解析为 Class 对象
        String parameterType = context.attributeValue("parameterType");
        Class<?> parameterTypeClass = resolveClass(parameterType);
        // 获取自定义语言驱动 lang 属性
        String lang = context.attributeValue("lang");
        // 获取对应的 LanguageDriver 实例
        LanguageDriver langDriver = getLanguageDriver(lang);
        // 处理 <selectKey> 节点（主键生成相关），并移除这些节点
        processSelectKeyNodes(id, parameterTypeClass, langDriver);
        // 解析 SQL 语句（此时 <selectKey> 已处理完毕）
        KeyGenerator keyGenerator;
        // 构建 selectKey 语句的 id
        String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
        // 应用命名空间，得到完整 id
        keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
        // 如果配置中已存在 keyGenerator，则直接获取
        if (configuration.hasKeyGenerator(keyStatementId)) {
            keyGenerator = configuration.getKeyGenerator(keyStatementId);
        } else {
            // 否则根据 useGeneratedKeys 属性和 SQL 类型判断是否使用自动生成主键
            Boolean useGeneratedKeys = getBooleanAttributeOfDom4jElement(context, "useGeneratedKeys");
            useGeneratedKeys = useGeneratedKeys != null ? useGeneratedKeys : configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType);
            // 决定使用哪种 KeyGenerator
            keyGenerator = useGeneratedKeys ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
        }
        // 通过 LanguageDriver 创建 SqlSource 对象，封装 SQL 语句和参数信息
        SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
        // 获取 resultType 属性的值，并解析为 Class 对象
        String resultType = context.attributeValue("resultType");
        Class<?> resultTypeClass = resolveClass(resultType);
        // 获取 resultMap 属性
        String resultMap = context.attributeValue("resultMap");
        /**
         * TODO:
         *  以下代码主要是为了从 XML 节点中获取配置的主键相关属性，并最终调用 MapperBuilderAssistant 的 addMappedStatement 方法来注册 MappedStatement 对象。
         *      1.在 XML 中，主键相关的配置属性主要有 keyProperty 和 keyColumn，这两个属性分别用于指定主键对应的 Java 对象属性名和数据库表列名。
         *      2.通过 context.attributeValue("keyProperty") 和 context.attributeValue("keyColumn") 方法，可以从当前的 XML 节点中获取这两个属性的值。
         *      3.keyProperty 表示主键在 Java 对象中的属性名，而 keyColumn 则表示主键在数据库表中的列名。
         *      4.keyProperty 和 keyColumn 的值是一个字符串，可能包含多个属性或列名，使用逗号分隔，例如 {@code <select keyProperty="id,name" keyColumn="user_id,user_name">}。
         *      5.获取到 keyProperty 和 keyColumn 后，接下来就是调用 builderAssistant.addMappedStatement 方法来注册 MappedStatement 对象。
         *      6.在 {@link MapperBuilderAssistant#addMappedStatement} 方法中，会使用 {@link MappedStatement.Builder} 去构建 MappedStatement 对象，并将 keyProperty 和 keyColumn 传递给它。
         *      7.在 {@link MappedStatement.Builder} 中，会使用 {@link MappedStatement.Builder#delimitedStringToArray(String)} 方法将 keyProperty 和 keyColumn 字符串转换为字符串数组，方便后续处理。
         */
        // 获取 keyProperty 属性（主键属性）
        String keyProperty = context.attributeValue("keyProperty");
        // 获取 keyColumn 属性（主键列名）
        String keyColumn = context.attributeValue("keyColumn");
        // 使用 MapperBuilderAssistant 构建 MappedStatement 对象
        builderAssistant.addMappedStatement(id, sqlSource, sqlCommandType, parameterTypeClass, resultMap, resultTypeClass, flushCache, keyGenerator, keyProperty, keyColumn, databaseId, langDriver);
    }

    /**
     * 处理 <selectKey> 节点（用于主键生成相关配置）
     *
     * @param id                 当前 SQL 语句节点的 id 属性值
     * @param parameterTypeClass 参数类型的 Class 对象
     * @param langDriver         语言驱动实例
     */
    private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {
        // 获取当前 SQL 节点下所有的 <selectKey> 子节点，返回为列表
        List<Element> selectKeyNodes = context.elements("selectKey");
        // 如果全局配置中指定了 databaseId（即当前环境有数据库标识）
        if (configuration.getDatabaseId() != null) {
            // 只解析 databaseId 匹配的 <selectKey> 节点
            parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
        }
        // 再解析没有 databaseId 限定的 <selectKey> 节点（兼容无 databaseId 的情况）
        parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);
        // 处理完后，从 XML 节点树中移除所有 <selectKey> 节点，避免后续重复解析
        removeSelectKeyNodes(selectKeyNodes);
    }

    /**
     * 解析 <selectKey> 节点列表，并为每个节点生成对应的主键生成语句
     *
     * @param parentId             父节点的 id 属性值
     * @param list                 <selectKey> 节点列表
     * @param parameterTypeClass   参数类型的 Class 对象
     * @param langDriver           语言驱动实例
     * @param skRequiredDatabaseId 所需匹配的 databaseId
     */
    private void parseSelectKeyNodes(String parentId, List<Element> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) {
        // 遍历所有 <selectKey> 节点
        for (Element nodeToHandle : list) {
            // 构建 selectKey 语句的 id（父 id + "_SelectKey" 后缀）
            String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
            // 获取当前 <selectKey> 节点的 databaseId 属性
            String databaseId = nodeToHandle.attributeValue("databaseId");
            // 判断 databaseId 是否匹配当前环境
            if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {
                // 如果匹配，则解析该 <selectKey> 节点
                parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);
            }
        }
    }

    /**
     * 解析单个 <selectKey> 节点，生成主键查询语句并注册 KeyGenerator
     *
     * @param id                 selectKey 语句的 id
     * @param nodeToHandle       当前 <selectKey> 节点
     * @param parameterTypeClass 参数类型的 Class 对象
     * @param langDriver         语言驱动实例
     * @param databaseId         当前节点的 databaseId 属性值
     */
    private void parseSelectKeyNode(String id, Element nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
        // 获取 <selectKey> 节点的 resultType 属性（主键类型）
        String resultType = nodeToHandle.attributeValue("resultType");
        // 解析主键类型对应的 Class
        Class<?> resultTypeClass = resolveClass(resultType);
        // todo:源码这里存在获取 statementType 属性的逻辑，暂时不实现该功能。

        // 获取 keyProperty 属性（主键属性名）
        String keyProperty = nodeToHandle.attributeValue("keyProperty");
        // 获取 keyColumn 属性（主键列名）
        String keyColumn = nodeToHandle.attributeValue("keyColumn");
        // 获取 order 属性，判断主键生成语句是在插入前执行还是后执行，默认为 AFTER
        boolean executeBefore = "BEFORE".equals(getStringAttributeOfDom4jElement(nodeToHandle, "order", "AFTER"));

        // todo:源码这里还存在其他处理逻辑，暂时不实现该功能。
        // 默认使用 NoKeyGenerator
        KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
        // 不刷新缓存
        boolean flushCache = false;
        // 默认没有指定结果集映射名称
        String resultMap = null;

        // 通过语言驱动生成 SqlSource（主键查询 SQL 的抽象表示）
        SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
        // selectKey 节点的 SQL 类型始终为 SELECT
        SqlCommandType sqlCommandType = SqlCommandType.SELECT;

        // 注册 MappedStatement（主键查询语句）到 MyBatis 配置，TODO:主要这里会将 <selectKey> 节点解析成 MappedStatement 对象，并注册到配置中。
        builderAssistant.addMappedStatement(id, sqlSource, sqlCommandType, parameterTypeClass, resultMap, resultTypeClass, flushCache, keyGenerator, keyProperty, keyColumn, databaseId, langDriver);

        // 应用命名空间，得到完整 id
        id = builderAssistant.applyCurrentNamespace(id, false);

        // 获取刚刚注册的 MappedStatement（主键查询语句）
        MappedStatement keyStatement = configuration.getMappedStatement(id);
        // 注册 KeyGenerator（主键生成器），用于后续插入时自动生成主键
        configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
    }

    // 移除所有 <selectKey> 节点，避免后续重复解析
    private void removeSelectKeyNodes(List<Element> selectKeyNodes) {
        // 遍历所有 <selectKey> 节点
        for (Element nodeToHandle : selectKeyNodes) {
            // 从父节点中移除当前 <selectKey> 节点
            nodeToHandle.getParent().remove(nodeToHandle);
        }
    }

    /**
     * 判断当前 SQL 节点的 databaseId 是否与所需的 databaseId 匹配
     *
     * @param id                 {@code <select>, <insert>，<update>，<delete>} 等节点的 id 属性值
     * @param databaseId         当前节点的 databaseId 属性值
     * @param requiredDatabaseId 所需匹配的 databaseId
     * @return 是否匹配
     */
    private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
        // 如果要求指定 databaseId（即 requiredDatabaseId 不为 null）
        if (requiredDatabaseId != null) {
            // 只有当当前节点的 databaseId 与所需的完全相等时才匹配
            return requiredDatabaseId.equals(databaseId);
        }
        // 如果未要求指定 databaseId，但当前节点有 databaseId，则不匹配（只处理无 databaseId 的节点）
        if (databaseId != null) {
            return false;
        }
        // 将 id 应用当前命名空间，得到完整 id
        id = builderAssistant.applyCurrentNamespace(id, false);
        // 如果配置中还没有这个 id 的语句，说明可以解析当前节点
        if (!this.configuration.hasStatement(id)) {
            return true;
        }
        /**
         * TODO:
         *  疑问:为什么还需要根据{@code <select>, <insert>，<update>，<delete>} 等节点的 id 属性值再去配置中获取一次 MappedStatement 呢？
         *  分析:
         *      因为在 MyBatis 中，可能会存在同名的 SQL 语句（即具有相同 id 的语句），但它们可能针对不同的数据库有不同的实现。
         *      这种情况下，通过 databaseId 属性可以区分这些同名语句，以确保在运行时选择正确的实现。
         *      场景:
         *          1.多数据库支持:在一个应用程序中，可能需要支持多个数据库（如 MySQL、Oracle、PostgreSQL 等）。每个数据库可能有特定的 SQL 语法或优化方式。
         *          2.同名语句:为了简化配置和维护，开发者可能会为不同数据库编写同名的 SQL 语句，但通过 databaseId 来区分它们。
         *      解决方案:
         *          因此，在解析 SQL 语句节点时，需要检查配置中是否已经存在同名的 MappedStatement。
         *          如果存在，则需要进一步检查其 databaseId 属性，以决定是否解析当前节点。
         *      规则:
         *          1.如果当前节点的 databaseId 与所需的 databaseId 不匹配，则跳过当前节点。
         *          2.如果当前节点没有 databaseId，则只有在配置中不存在同名语句，或者已存在的同名语句也没有 databaseId 时，才解析当前节点。
         *      总结:
         *          通过这种机制，MyBatis 能够灵活地处理多数据库环境下的同名 SQL 语句，确保在运行时选择正确的实现，避免冲突和错误。
         *      判断逻辑:
         *          - 如果配置中已经存在同名的 MappedStatement:
         *              1.如果之前的语句有 databaseId，则跳过当前节点，避免重复解析。
         *              2.如果之前的语句没有 databaseId，则当前节点可以被解析。
         *          - 如果配置中不存在同名的 MappedStatement，则当前节点可以被解析。
         */
        MappedStatement previous = this.configuration.getMappedStatement(id);
        return previous.getDatabaseId() == null;
    }

    /**
     * 获取对应的 LanguageDriver 实例（用于解析 SQL 脚本的语言驱动）
     *
     * @param lang XML 中的 lang 属性值，指定自定义的语言驱动类名
     * @return 对应的 LanguageDriver 实例
     */
    private LanguageDriver getLanguageDriver(String lang) {
        // 声明一个 LanguageDriver 类型的类对象，初始为 null
        Class<? extends LanguageDriver> langClass = null;
        // 如果 lang 不为 null，说明用户自定义了语言驱动
        if (lang != null) {
            // 通过 resolveClass 方法将类名字符串解析为 Class 对象
            langClass = resolveClass(lang);
        }
        // 从全局配置中获取对应的 LanguageDriver 实例（如果 langClass 为 null，则返回默认的驱动）
        return configuration.getLanguageDriver(langClass);
    }
}
